# -*- coding: utf-8 -*-

# Copyright (C) 2009 Axel Tillequin (bdcht3@gmail.com)
# This code is part of Amoco
# published under GPLv2 license

import gtk
import math

from goocanvas import *

# connectors CX are embedded inside node views. These objects are drawn on
# the node's surface and exists only as sub-objects of their node. CX are used
# as 'ports' for edges connected with a node. A node can have several such CX
# and can register or unregister its edges on such CX.
class CX(Rect):
    def __init__(self, e=None):
        Rect.__init__(self, width=3, height=3)
        self.set_properties(line_width=0, fill_color="red")
        # self.props.visibility=False
        # list of edges connected to this CX:
        self.registered = []
        if e != None:
            self.register(e)
        # self.connect('event',CX.eventhandler)

    def set_wh(self, wh):
        self.set_properties(width=wh[0], height=wh[1])

    def get_wh(self):
        return self.get_properties("width", "height")

    wh = property(get_wh, set_wh)

    def getpos(self):
        bb = self.get_bounds()
        return (bb.x1, bb.y1)

    def getcenter(self):
        xy = self.getpos()
        wh = self.get_wh()
        return (xy[0] + wh[0] / 2.0, xy[1] + wh[1] / 2.0)

    # manage Edge_basic that are using this CX:
    def register(self, item):
        self.registered.append(item)

    def unregister(self, item):
        self.registered.remove(item)

    def eventhandler(*args):
        print("CX eventhandler on", args)


# ------------------------------------------------------------------------------
# decorators for eventhandlers: this sets the 'clicked' field to the mouse
# button id, and moves the object along with mouse-1 movements.
def mouse1moves(h):
    def wrapper(self, item, e):
        # self.last_msec = [0]
        if e.type is gtk.gdk.BUTTON_PRESS:
            self.clicked = e.button
            self.oldx, self.oldy = e.get_coords()
            # self.last_msec[0] = 0.
        elif e.type is gtk.gdk.BUTTON_RELEASE:
            self.clicked = 0
        elif e.type is gtk.gdk.MOTION_NOTIFY:
            # if abs(e.time - self.last_msec[0])<10: return False
            # self.last_msec[0]=e.time
            if self.clicked == 1:
                newx, newy = e.get_coords()
                tx, ty = newx - self.oldx, newy - self.oldy
                self.translate(tx, ty)
                self.notify("transform")
        return h(self, item, e)

    return wrapper


# ------------------------------------------------------------------------------
# This is a 'circle' shaped view for nodes.
class Node_basic(Group):
    def __init__(self, name="?", r=10):
        Group.__init__(self)
        self.el = Ellipse(
            parent=self, fill_color="gray88", stroke_color="black", line_width=2
        )
        # extra:
        self.alpha = 1.0
        self.r = r
        self.label = Text(
            parent=self,
            text="[%s]" % name,
            font="monospace, bold, 8",
            fill_color="blue",
            anchor=gtk.ANCHOR_CENTER,
        )
        # self.label.props.visibility = False
        # edges connectors:
        self.cx = []
        # events:
        self.connect("enter-notify-event", Node_basic.eventhandler)
        self.connect("leave-notify-event", Node_basic.eventhandler)
        self.connect("button-press-event", Node_basic.eventhandler)
        self.connect("button-release-event", Node_basic.eventhandler)
        self.connect("motion-notify-event", Node_basic.eventhandler)
        # clicked: 1=mouse1, 2=mouse2, 3=mouse3
        self.clicked = 0
        self.connect("notify::transform", Node_basic.notifyhandler)
        self.raise_(None)
        self.w, self.h = self.wh

    # prop:
    def set_r(self, r):
        self._r = r
        self.el.set_properties(radius_x=r, radius_y=r)

    def get_r(self):
        return self._r

    r = property(get_r, set_r)

    def set_wh(self, wh):
        pass

    def get_wh(self):
        bb = self.get_bounds()
        self.w, self.h = (bb.x2 - bb.x1, bb.y2 - bb.y1)
        return (self.w, self.h)

    wh = property(get_wh, set_wh)

    def set_xy(self, xy):
        self.props.x, self.props.y = xy

    def get_xy(self):
        return (self.props.x, self.props.y)

    xy = property(get_xy, set_xy)

    # put the cx pt at the intersection between the circle shape of Node_basic
    # and the radius from centre to pt 'topt'.
    def intersect(self, topt, cx):
        assert self.find_child(cx) != -1
        # get cx pos on canvas:
        bb = self.get_bounds()
        w, h = self.get_wh()
        x1, y1 = (w / 2.0, h / 2.0)
        # intersect with target pt:
        x2, y2 = topt[0] - bb.x1, topt[1] - bb.y1
        theta = math.atan2(y2 - y1, x2 - x1)
        newx = math.cos(theta) * self._r
        newy = math.sin(theta) * self._r
        cx.set_properties(x=newx, y=newy)
        self._angle = theta

    def set_alpha(self, a):
        color = self.props.fill_color_rgba & 0xFFFFFF00
        self.props.fill_color_rgba = color + (int(a * 255.0) & 0xFF)

    def get_alpha(self):
        return (self.props.fill_color_rgba & 0xFF) / 255.0

    alpha = property(get_alpha, set_alpha)

    @mouse1moves
    def eventhandler(self, item, e):
        if e.type is gtk.gdk.ENTER_NOTIFY:
            self.props.line_width = 2.0
        elif e.type is gtk.gdk.LEAVE_NOTIFY:
            self.props.line_width = 1.0
        return False

    def notifyhandler(self, prop):
        # print("notify %s on "%(prop.name,self))
        for cx in self.cx:
            for e in cx.registered:
                e.update_points()


# ------------------------------------------------------------------------------
class Edge_basic(Polyline):
    def __init__(self, n0, n1, head=False):
        self.n = [n0, n1]
        x0, y0 = n0.xy
        x1, y1 = n1.xy
        self.cx = [CX(self), CX(self)]
        n0.cx.append(self.cx[0])
        n0.add_child(self.cx[0])
        n1.cx.append(self.cx[1])
        n1.add_child(self.cx[1])
        Polyline.__init__(self, points=Points([(x0, y0), (x1, y1)]))
        self.set_properties(
            close_path=False,
            stroke_color="black",
            end_arrow=True,
            fill_pattern=None,
            line_width=2,
        )
        if head:
            self.set_properties(end_arrow=True)
        self.update_points()
        self.lower(None)
        self.clicked = 0

    def setpath(self, l):
        self.props.points = Points(l)
        self.update_points()

    def update_points(self):
        pts = self.props.points.coords
        self.n[0].intersect(topt=pts[1], cx=self.cx[0])
        self.n[1].intersect(topt=pts[-2], cx=self.cx[-1])
        self.cx[-1].set_properties(fill_color="blue")
        cx = self.cx[0].getcenter()
        pts[0] = cx
        cx = self.cx[-1].getcenter()
        pts[-1] = cx
        self.props.points = Points(pts)


# ------------------------------------------------------------------------------
class Edge_curve(Path):
    def __init__(self, n0, n1, head=True):
        self.n = [n0, n1]
        self.has_head = head
        self.cx = [CX(self), CX(self)]
        n0.cx.append(self.cx[0])
        n0.add_child(self.cx[0])
        n1.cx.append(self.cx[1])
        n1.add_child(self.cx[1])
        Path.__init__(self)
        self.set_properties(
            stroke_color="black",
            fill_pattern=None,
            line_width=2,
            line_cap=1,
            line_join=1,
        )
        self.splines = [[n0.xy, n1.xy]]
        self.update_points()
        self.lower(None)
        self.clicked = 0

    def make_data(self):
        p0 = self.splines[0][0]
        data = "M %d %d" % p0
        for s in self.splines:
            if len(s) == 2:
                data += " L %d %d" % s[1]
            else:
                data += " C %d %d" % s[1]
                data += " %d %d" % s[2] + " %d %d" % s[3]
        if self.has_head:
            s = self.splines[-1]
            p = s[-1]
            dx = p[0] - s[-2][0]
            dy = p[1] - s[-2][1]
            l = math.sqrt(dx * dx + dy * dy)
            dx, dy = dx / l * 6, dy / l * 6
            data += " l %d %d" % (-dx - dy, -dy + dx)
            data += " M %d %d" % p
            data += " l %d %d" % (-dx + dy, -dy - dx)
            data += " M %d %d" % p
        return data

    def setpath(self, l):
        try:
            self.splines = self.setcurve(l)
        except:
            pass

    def update_points(self):
        try:
            spl0 = self.splines[0]
            spl1 = self.splines[-1]
            # move CX to intersection between edge and Node border:
            self.n[0].intersect(topt=spl0[-1], cx=self.cx[0])
            self.n[1].intersect(topt=spl1[0], cx=self.cx[1])
            cx = self.cx[0].getcenter()
            spl0[0] = cx
            cx = self.cx[1].getcenter()
            spl1[-1] = cx
            self.set_properties(data=self.make_data())
        except:
            pass


# ------------------------------------------------------------------------------
class Node_codeblock(Group):
    def __init__(self, code):
        Group.__init__(self, can_focus=True)
        self.codebox = Rect(parent=self, can_focus=True)
        self.code = Text(
            parent=self,
            text=code,
            font="monospace, 10",
            use_markup=True,
            can_focus=True,
            fill_color="black",
        )
        self.padding = 4
        bbink, bblogic = self.code.get_natural_extents()
        w = (bblogic[2] * 0.001) + 2 * self.padding
        h = (bblogic[3] * 0.001) + 2 * self.padding
        self.codebox.set_properties(width=w, height=h)
        self.codebox.set_properties(
            fill_color="white", stroke_color="black", line_width=1
        )
        self.code.raise_(self.codebox)
        # shadow :
        self.shadow = s = 4
        self.code.set_properties(x=self.padding, y=self.padding)
        self.shadbox = Rect(
            x=s, y=s, width=w, height=h, fill_color="grey44", line_width=0
        )
        self._wh = (w + s, h + s)
        self.w, self.h = self._wh
        self.cx = []
        self.add_child(self.shadbox, 0)
        self.shadbox.lower(self.codebox)
        # events:
        self.clicked = 0
        self.connect("enter-notify-event", Node_codeblock.eventhandler)
        self.connect("leave-notify-event", Node_codeblock.eventhandler)
        self.connect("button-press-event", Node_codeblock.eventhandler)
        self.connect("button-release-event", Node_codeblock.eventhandler)
        self.connect("key-press-event", Node_codeblock.eventhandler)
        self.connect("key-release-event", Node_codeblock.eventhandler)
        self.connect("motion-notify-event", Node_codeblock.eventhandler)
        self.connect("notify::transform", Node_codeblock.notifyhandler)

    def set_wh(self, wh):
        pass

    def get_wh(self):
        return self._wh

    wh = property(get_wh, set_wh)

    # xy property is bound to center of object
    def get_xy(self):
        w, h = self.codebox.get_properties("width", "height")
        return (self.props.x + w / 2.0, self.props.y + h / 2.0)

    def set_xy(self, xy):
        w, h = self.codebox.get_properties("width", "height")
        self.props.x, self.props.y = xy[0] - w / 2.0, xy[1] - h / 2.0

    xy = property(get_xy, set_xy)

    def intersect(self, topt, cx):
        assert self.find_child(cx) != -1
        bb = self.get_bounds()
        w, h = self.codebox.get_properties("width", "height")
        x1, y1 = (w / 2.0, h / 2.0)
        x2, y2 = topt[0] - bb.x1, topt[1] - bb.y1
        # now try all 4 segments of self rectangle:
        S = [
            ((x1, y1), (x2, y2), (0, 0), (w, 0)),
            ((x1, y1), (x2, y2), (w, 0), (w, h)),
            ((x1, y1), (x2, y2), (0, h), (w, h)),
            ((x1, y1), (x2, y2), (0, h), (0, 0)),
        ]
        for segs in S:
            xy = intersect2lines(*segs)
            if xy != None:
                cx.set_properties(x=xy[0], y=xy[1])
                break

    def highlight_on(self, style=None):
        import re

        if style is None:
            style = {
                "addr": '<span foreground="blue">%s</span>',
                "code": '<span foreground="black">%s</span>',
                "mnem": '<span foreground="black" weight="bold">%s</span>',
                "strg": '<span foreground="DarkRed">%s</span>',
                "cons": '<span foreground="red">%s</span>',
                "comm": '<span foreground="DarkGreen">%s</span>',
            }
        lre = re.compile("(0x[0-9a-f]+ )('[0-9a-f]+' +)(.*)$")
        hcode = []
        for l in self.code.get_properties("text")[0].splitlines():
            if l.startswith("#"):
                hcode.append(style["comm"] % l)
            else:
                m = lre.match(l)
                if m is None:
                    return
                g = m.groups()
                s = [style["addr"] % g[0]]
                s += [style["strg"] % g[1]]
                s += [style["code"] % g[2]]
                hcode.append("".join(s))
        self.code.set_properties(text="\n".join(hcode))
        self.code.set_properties(use_markup=True)

    def highlight_off(self):
        import re

        lre = re.compile("<span [^>]+>(.*?)</span>")
        code = []
        for l in self.code.get_properties("text").splitlines():
            g = lre.findall(l)
            if len(g) > 0:
                code.append("".join(g))
        self.code.set_properties(text="\n".join(code))
        self.code.set_properties(use_markup=False)

    @mouse1moves
    def eventhandler(self, item, e):
        # print("*** CODEBLOCK EVENT =",e)
        if e.type is gtk.gdk.ENTER_NOTIFY:
            self.codebox.set_properties(line_width=2.0)
        elif e.type is gtk.gdk.LEAVE_NOTIFY:
            self.codebox.set_properties(line_width=1.0)
        return False

    def notifyhandler(self, prop):
        # print("notify %s on "%(prop.name,self))
        for cx in self.cx:
            for e in cx.registered:
                e.update_points()

    def resrc(self, code):
        self.code.props.text = code
        bb = self.code.get_bounds()
        w = (bb.x2 - bb.x1) + self.padding
        h = (bb.y2 - bb.y1) + self.padding
        self.codebox.set_properties(width=w, height=h)
        self.shadbox.set_properties(width=w, height=h)


def intersect2lines(X1, X2, X3, X4):
    (x1, y1) = X1
    (x2, y2) = X2
    (x3, y3) = X3
    (x4, y4) = X4
    b = (x2 - x1, y2 - y1)
    d = (x4 - x3, y4 - y3)
    det = b[0] * d[1] - b[1] * d[0]
    if det == 0:
        return None
    c = (x3 - x1, y3 - y1)
    t = float(c[0] * b[1] - c[1] * b[0]) / (det * 1.0)
    if t < 0.0 or t > 1.0:
        return None
    t = float(c[0] * d[1] - c[1] * d[0]) / (det * 1.0)
    if t < 0.0 or t > 1.0:
        return None
    x = x1 + t * b[0]
    y = y1 + t * b[1]
    return (x, y)
